// Description: 描述
package tools

import (
	"context"
	"sync"
	"time"

	"github.com/zeromicro/go-zero/core/fx"
	"github.com/zeromicro/go-zero/core/stores/redis"
	"github.com/zeromicro/go-zero/core/syncx"
	"go.opentelemetry.io/otel/trace"

	"gitlab.vspncd.com/mic/common/xcache"
)

func DoWithRetry(fn func() error, timeout ...time.Duration) error {
	var e error
	err := fx.DoWithRetry(func() (err error) {
		defer func() {
			if err == nil {
				return
			}

			if len(timeout) > 0 {
				time.Sleep(timeout[0])
			} else {
				time.Sleep(20 * time.Millisecond)
			}
		}()

		err = fn()
		if err == context.Canceled {
			e = err
			// 取消的情况，不做重试
			err = nil
			return
		}

		return
	})
	if err != nil {
		return err
	}

	return e
}

// If ternary operator
func If(condition bool, trueObj interface{}, falseObj interface{}) interface{} {
	if condition {
		return trueObj
	}
	return falseObj
}

// 自定义递增key设置过期时间
var CusIncrSetExpire = func() func(client *redis.Redis, key string, incrFn func() (int, error)) error {
	return func(client *redis.Redis, key string, incrFn func() (int, error)) error {
		expire, err := incrFn()
		if err != nil {
			return err
		}

		if expire > 0 {
			_ = DoWithRetry(func() error {
				return client.Expire(key, expire)
			})
		}

		return err
	}
}()

// 简单key设置过期时间
var IncrSetExpire = func() func(client *redis.Redis, key string, args ...int) (int64, error) {
	return func(client *redis.Redis, key string, args ...int) (v int64, err error) {
		stage := 1
		expire := 0

		if len(args) == 2 {
			expire = args[0]
			stage = args[1]
		} else if len(args) == 1 {
			expire = args[0]
		}

		if stage != 1 {
			v, err = client.Incrby(key, int64(stage))
		} else {
			v, err = client.Incr(key)
		}
		if err != nil {
			return
		}

		if expire > 0 && v == int64(stage) {
			_ = DoWithRetry(func() error {
				return client.Expire(key, expire)
			})
		}

		return
	}
}()

// 给redis的key设置过期时间；在一些需要设置redis复杂对象过期的场景下，结合进程内缓存可以有效减少io的开销
var OnceSetExpire = func() func(client *redis.Redis, key string, getExpireSecFn func() int) error {
	xc := xcache.NewLocalCache(500000)
	return func(client *redis.Redis, key string, getExpireSecFn func() int) error {
		// 获取过期秒数
		expire := getExpireSecFn()
		if expire <= 0 {
			return nil
		}

		_, ok := xc.Get(key)
		if ok {
			return nil
		}

		// 给key设置过期
		err := DoWithRetry(func() error {
			return client.Expire(key, expire)
		})
		if err != nil {
			return err
		}

		// 设置本地缓存
		xc.Set(key, 1, time.Duration(expire)*time.Second)

		return nil
	}
}()

// lua脚本执行器
var ExecLuaScript = func() func(client *redis.Redis, hashKey, script string, keys []string, args []interface{}) (interface{}, error) {
	g := syncx.NewSingleFlight()
	lock := sync.RWMutex{}
	data := make(map[string]string)

	Get := func(key string) (string, bool) {
		lock.RLock()
		defer lock.RUnlock()
		value, ok := data[key]
		return value, ok
	}
	Set := func(key, value string) {
		lock.Lock()
		defer lock.Unlock()
		data[key] = value
	}

	return func(client *redis.Redis, hashKey, script string, keys []string, args []interface{}) (interface{}, error) {
		value, err := g.Do(hashKey, func() (interface{}, error) {
			sha, ok := Get(hashKey)
			if ok {
				return sha, nil
			}

			_ = DoWithRetry(func() error {
				var e error
				sha, e = client.ScriptLoad(script)
				return e
			})

			if len(sha) > 0 {
				// 缓存到内存
				Set(hashKey, sha)
			}

			return sha, nil
		})
		if err != nil {
			// 这里是一个保护设置，保证在获取不到脚本hash的时候还能正常执行
			return client.Eval(script, keys, args...)
		}

		hashSha := value.(string)
		if len(hashSha) == 0 {
			// 这里是一个保护设置，保证在获取不到脚本hash的时候还能正常执行
			return client.Eval(script, keys, args...)
		}

		// 通过执行脚本hash，可以节约数据的传输
		val, err := client.EvalSha(hashSha, keys, args...)
		if err != nil && err != redis.Nil {
			// 如果失败，有可能是在集群环境中，打到没有缓存的节点上了，这里通过eval直接执行，执行成功以后，该节点会缓存该脚本，下次
			// 执行就直接可以运行EvalSha了
			return client.Eval(script, keys, args...)
		}

		return val, nil
	}
}()

func SpanIdFromContext(ctx context.Context) string {
	spanCtx := trace.SpanContextFromContext(ctx)
	if spanCtx.HasSpanID() {
		return spanCtx.SpanID().String()
	}
	return ""
}

func TraceIdFromContext(ctx context.Context) string {
	spanCtx := trace.SpanContextFromContext(ctx)
	if spanCtx.HasTraceID() {
		return spanCtx.TraceID().String()
	}
	return ""
}
